王子子的成长之路

改善 Python 程序的 91 个建议读书笔记 1

第 1 章 引论

建议 1:理解 Pythonic 概念

Pythonic
当你输入 import this 就会显示 zen of python

美丽胜于丑陋。
显式优于隐式。
简单比复杂好。
复合胜于复杂。
平面比嵌套好。
稀疏比密集好。
可读性是重要的。
特殊情况不足以打破规则。
虽然实用性胜过纯粹。
除了显示错误,错误永远不应该沉默。

代码风格
充分体现python动态语言的特色,类似于

# 变量交换  
a, b = b, a  
# 上下文管理  
with open(path, 'r') as f:  
    do_sth_with(f)  
# 不应当过分地追求奇技淫巧  
a = [1, 2, 3, 4]   
a[::-1] # 不推荐。好吧,自从学了切片我一直用的这个  
list(reversed(a))   # 推荐  

然后表扬了 Flask 框架,提到了 generator 之类的特性尤为 Pythonic,有个包和模块的约束:

  • 包和模块的命名采用小写、单数形式,而且短小
  • 包通常仅作为命名空间,如只含空的__init__.py文件

建议 2:编写 Pythonic 代码

避免劣化代码

  • 避免只用大小写区分不同的对象
  • 避免使用容易引起混淆的名称
  • 不要害怕过长的变量名

深入认识python有助于编写pythonic代码

  • 全面掌握 python 提供的特性,包括语言和库
  • 随着时间推移,要不断更新知识
  • 深入学习业界公认的 pythoni 代码
  • 编写符合 pep8 的代码规范(就是让你使用pycharm)

建议 3:理解 Python 与 C 语言的不同之处

  • Python 使用代码缩进的方式来分割代码块,不要混用 Tab 键和空格
  • Python 中单、双引号的效果相同(个人建议使用单引号,在面对其他语言的双引号源码时不必再转义)
  • 三元操作符:x if bool else y(原因是作者认为应该用可读性更好的方式表达)
  • 用其他方法替代 switch-case

建议 4:在代码中适当添加注释

  • 块和行注释仅仅注释复杂的操作、算法等
  • 注释和代码隔开一段距离
  • 给外部可访问的函数和方法添加文档注释
  • 推荐在文件头中包含 copyright 申明、模块描述等

另外,编写代码应该朝代码即文档的方向进行,但仍应该注重注释的使用

建议 5:通过适当添加空行使代码布局更为优雅、合理

  • 表达完一个完整思路后,应该用空白行间隔,尽量不要在一段代码中说明几件事。
  • 尽量保持上下文的易理解性,比如调用者在上,被调用者在下
  • 避免过长的代码行,超过80个字符应该使用行连接换行(还是让你使用pycharm)
  • 水平对齐毫无意义,不要用多余空格保持对齐
  • 空格的使用要能够在需要使用时强调警示读者(符合PEP8规范)

建议 6:编写函数的 4 个原则

  1. 函数设计要尽量短小,嵌套层次不宜过深
  2. 函数申明应该做到合理、简单、易于使用
  3. 函数参数设计应该考虑向下兼容
  4. 一个函数只做一件事,尽量保证函数语句粒度的一致性

Python 中函数设计的好习惯还包括:不要在函数中定义可变对象作为默认值,使用异常替换返回错误,保证通过单元测试等。

# 关于函数设计的向下兼容  
def readfile(filename):         # 第一版本  
    pass  
def readfile(filename, log):    # 第二版本  
    pass  
def readfile(filename, logger=logger.info):     # 合理的设计  
    pass  

最后还有个函数可读性良好的例子:

def GetContent(ServerAdr, PagePath):  
    http = httplib.HTTP(ServerAdr)  
    http.putrequest('GET', PagePath)  
    http.putheader('Accept', 'text/html')  
    http.putheader('Accept', 'text/plain')  
    http.endheaders()  
    httpcode, httpmsg, headers = http.getreply()  
    if httpcode != 200:  
        raise "Could not get document: Check URL and Path."  
    doc = http.getfile()  
    data = doc.read()       # 此处是不是应该使用 with ?  
    doc.close  
    return data  
  
def ExtractData(inputstring, start_line, end_line):  
    lstr = inputstring.splitlines()             # split  
    j = 0  
    for i in lstr:  
        j += 1  
        if i.strip() == start_line: slice_start = j  
        elif i.strip() == end_line: slice_end = j  
    return lstr[slice_start:slice_end]  
  
def SendEmail(sender, receiver, smtpserver, username, password, content):  
    subject = "Contented get from the web"  
    msg = MIMEText(content, 'plain', 'utf-8')  
    msg['Subject'] = Header(subject, 'utf-8')  
    smtp = smtplib.SMTP()  
    smtp.connect(smtpserver)  
    smtp.login(username, password)  
    smtp.sendmail(sender, receiver, msg.as_string())  
    smtp.quit()  
  

建议 7:将常量集中到一个文件

在Python中应当如何使用常量:

  • 常量名全部大写
  • 将存放常量的文件命名为constant.py

示例为:

class _const:  
    class ConstError(TypeError): pass  
    class ConstCaseError(ConstError): pass  
  
    def __setattr__(self, name, value):  
        if self.__dict__.has_key(name):  
            raise self.ConstError, "Can't change const.%s" % name  
        if not name.isupper():  
            raise self.ConstCaseError, \  
                    'const name "%s" is not all uppercase' % name  
        self.__dict__[name] = value  
  
import sys  
sys.modules[__name__] = _const()  
import const  
const.MY_CONSTANT = 1  
const.MY_SECOND_CONSTANT = 2  
const.MY_THIRD_CONSTANT = 'a'  
const.MY_FORTH_CONSTANT = 'b'  
  

其他模块中引用这些常量时,按照如下方式进行即可:

from constant import const  
print(const.MY_CONSTANT)  

第 2 章 编程惯用法

建议 8:利用 assert 语句来发现问题

断言的判断会对性能有所影响,因此要分清断言的使用场合:

  • 断言应使用在正常逻辑无法到达的地方或总是为真的场合
  • python本身异常处理能解决的问题不需要用断言
  • 不要使用断言检查用户输入,而使用条件判断
  • 在函数调用后,当需要确认返回值是否合理时使用断言
  • 当条件是业务的先决条件时可以使用断言

代码示例:

>>> y = 2  
>>> assert x == y, "not equals"  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
AssertionError: not equals  
>>> x = 1  
>>> y = 2  
# 以上代码相当于  
>>> if __debug__ and not x == y:  
...     raise AssertionError("not equals")  
...   
Traceback (most recent call last):  
  File "<stdin>", line 2, in <module>  
AssertionError: not equals  
  

运行是加入-O参数可以禁用断言。

建议 9:数据交换的时候不推荐使用中间变量

>>> Timer('temp = x; x = y; y = temp;', 'x = 2; y = 3').timeit()  
0.059251302998745814  
>>> Timer('x, y = y, x', 'x = 2; y = 3').timeit()  
0.05007316499904846  

对于表达式x, y = y, x,在内存中执行的顺序如下:
1. 先计算右边的表达式y, x,因此先在内存中创建元组(y, x),其标识符和值分别为y, x及其对应的值,其中y和x是在初始化已经存在于内存中的对象。
2. 计算表达式左边的值并进行赋值,元组被依次分配给左边的标识符,通过解压缩,元组第一标识符y分配给左边第一个元素x,元组第二标识符x分配给左边第一个元素y,从而达到交换的目的。

(简单来说,直接交换符合pythonic且性能最佳,这么做就对了)

建议 10:充分利用 Lazy evaluation 的特性

(就是生成器)
Lazy evaluation常被译为延迟计算,体现在用 yield 替换 return 使函数成为生成器,好处主要有两方面:

  1. 避免不必要的计算,带来性能提升
  2. 节省空间,使无限循环的数据结构成为可能
def fib():  
    a, b = 0, 1  
    while True:  
        yield a  
        a, b = b, a + b  

建议 11:理解枚举替代实现的缺陷

使用 flufl.enum 实现枚举

建议 12:不推荐使用 type 来进行类型检查

使用 isinstance 来进行类型检查(注意上下包含关系就行)

建议 13:尽量转换为浮点类型后再做除法

py2.x:转换浮点类型后再做除法

建议 14:警惕 eval() 的安全漏洞

eval具有安全漏洞,建议使用安全性更好的ast.literal_eval。

建议 15:使用 enumerate() 获取序列迭代的索引和值

>>> li = ['a', 'b', 'c', 'd', 'e']  
>>> for i, e in enumerate(li):  
...     print('index: ', i, 'element: ', e)  
...   
index:  0 element:  a  
index:  1 element:  b  
index:  2 element:  c  
index:  3 element:  d  
index:  4 element:  e  
# enumerate(squence, start=0) 内部实现  
def enumerate(squence, start=0):  
    n = start  
    for elem in sequence:  
        yield n, elem   # 666  
        n += 1  
# 明白了原理我们自己也来实现一个反序的  
def reversed_enumerate(squence):  
    n = -1  
    for elem in reversed(sequence):  
        yield len(sequence) + n, elem  
        n -= 1  
  

(此方式相比从列表里放索引取值更加优雅)

建议 16:分清 == 与 is 的适用场景

比较有趣的:

>>> s1 = 'hello world'  
>>> s2 = 'hello world'  
>>> s1 == s2  
True  
>>> s1 is s2  
False  
>>> s1.__eq__(s2)  
True  
>>> a = 'Hi'  
>>> b = 'Hi'  
>>> a == b  
True  
>>> a is b  
True  
  

为了提高系统性能,对于较小的字符串会保留其值的一个副本,当创建新的字符串时直接指向该副本,所以a和b的 id 值是一样的,同样对于小整数[-5, 257)也是如此:

注意is不相当于 ==, is 是对 id 方法做的 == 。

建议 17:考虑兼容性,尽可能使用 Unicode

python2.x 这是无敌深坑,需要刻苦学习掌握(python3偶尔也会碰到这种问题,但避免了大多数这种可能)

建议 18:构建合理的包层次来管理 module

(__init__是对包的头文件定制) 本质上每一个 Python 文件都是一个模块,使用模块可以增强代码的可维护性和可重用性,在较大的项目中,我们需要合理地组织项目层次来管理模块,这就是包(Package)的作用。

一句话说包:一个包含__init__.py 文件的目录。包中的模块可以通过.进行访问,即包名.模块名。那么这init.py文件有什么用呢?最明显的作用就是它区分了包和普通目录,在该文件中申明模块级别的 import 语句从而变成了包级别可见,另外在该文件中定义__all__变量,可以控制需要导入的子包或模块。

这里给出一个较为合理的包组织方式,是FlaskWeb 开发:基于Python的Web应用开发实战一书中推荐而来的:

|-flasky  
    |-app/                      # Flask 程序  
        |-templates/            # 存放模板  
        |-static/               # 静态文件资源  
        |-main/  
            |-__init__.py  
            |-errors.py         # 蓝本中的错误处理程序  
            |-forms.py          # 表单对象  
            |-views.py          # 蓝本中定义的程序路由  
        |-__init__.py  
        |-email.py              # 电子邮件支持  
        |-models.py             # 数据库模型  
    |-migrations/               # 数据库迁移脚本  
    |-tests/                    # 单元测试  
        |-__init__.py  
        |-test*.py  
    |-venv/                     # 虚拟环境  
    |-requirements/  
        |-dev.txt               # 开发过程中的依赖包  
        |-prod.txt              # 生产过程中的依赖包  
    |-config.py                 # 储存程序配置  
    |-manage.py                 # 启动程序以及其他的程序任务  
  

第 3 章:基础语法

建议 19:有节制地使用 from…import 语句

Python 提供三种方式来引入外部模块:import语句、from…import语句以及__import__函数,其中__import__函数显式地将模块的名称作为字符串传递并赋值给命名空间的变量。

使用import需要注意以下几点:

  • 优先使用import a的形式
  • 有节制地使用from a import A
  • 尽量避免使用from a import *

为什么呢?我们来看看 Python 的 import 机制,Python 在初始化运行环境的时候会预先加载一批内建模块到内存中,同时将相关信息存放在sys.modules中,我们可以通过 sys.modules.items() 查看预加载的模块信息,当加载一个模块时,解释器实际上完成了如下动作:

  1. 在 sys.modules 中搜索该模块是否存在,如果存在就导入到当前局部命名空间,如果不存在就为其创建一个字典对象,插入到 sys.modules 中。
  2. 加载前确认是否需要对模块对应的文件进行编译,如果需要则先进行编译。
  3. 执行动态加载,在当前命名空间中执行编译后的字节码,并将其中所有的对象放入模块对应的字典中。
>>> dir()  
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']  
>>> import test  
testing module import  
>>> dir()  
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test']  
>>> import sys  
>>> 'test' in sys.modules.keys()  
True  
>>> id(test)  
140367239464744  
>>> id(sys.modules['test'])  
140367239464744  
>>> dir(test)  
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']  
>>> sys.modules['test'].__dict__.keys()  
dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])  

从上可以看出,对于用户自定义的模块,import 机制会创建一个新的 module。 将其加入当前的局部命名空间中,同时在 sys.modules 也加入该模块的信息,但本质上是在引用同一个对象,通过test.py所在的目录会多一个字节码文件。

(这节说的是,盲目使用from…import…会带来:
1. 命名空间冲突
2. 循环嵌套导入)

建议 20:优先使用 absolute import 来导入模块

(py3 中 relative import方法已被移除,不用操心)